-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MIR-borrowck] Two phase borrows #46537
Conversation
r? @arielb1 (rust_highfive has picked a reviewer for you, use r? to override) |
|
||
fn main() { | ||
let mut v = vec![0, 1, 2]; | ||
v.push(v.len()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huzzah.
lookup_result: LookupResult, | ||
each_child: F) | ||
where F: FnMut(MovePathIndex) | ||
pub(crate) fn place_needs_drop<'a, 'gcx, 'tcx>(tcx: TyCtxt<'a, 'gcx, 'tcx>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: formatting is funky. I'm moving these days towards rustfmt-style:
pub(crate) fn place_needs_drop(
tcx: TyCTxt<...>
...,
) -> bool {
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left various documentation nits.
I'd personally prefer to tweak the "use closures for everything" change to dataflow as well; either to keep the tcx or to thread through the required state as a generic that implements a trait with the method definitions, rather than various closures. I found closures a bit confusing because (a) they move the code out from the fn body into its caller, but (imo) it sort of logically belongs in the callee; (b) they suggest that caller can alter this behavior at will, but I'm not sure that's true; (c) they tend to play poorly with the borrow checker anyway vs a trait, since the borrow checker won't allow two closures to share a mutable borrow (since it doesn't know they don't reach one another).
Anyway, there is one test I didn't see. We want to make sure that people don't start a shared borrow during the reservation period that then continues to be used after the mutable borrow has been activated. Did you have a test for this that I missed? (I actually don't know if the code will handle this correctly or not. I think maybe not, because I believe it requires some kind of special check in the borrow checker at activation time.)
fn read(_: &i32) { }
fn ok() {
let mut x = 3;
let y = &mut x;
{ let z = &x; read(z); }
*y += 1;
}
fn not_ok() {
let mut x = 3;
let y = &mut x;
let z = &x;
*y += 1;
read(z); //~ ERROR somewhere in here
}
erased_ty.needs_drop(gcx, param_env) | ||
} | ||
|
||
pub(crate) fn on_lookup_result_bits<'tcx, F, U>(move_data: &MoveData<'tcx>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd love to see a comment. e.g. what the heck is has_uniform_drop_state
supposed to mean?
move_path_index: MovePathIndex, | ||
mut each_child: F) | ||
where F: FnMut(MovePathIndex) | ||
pub(crate) fn on_all_children_bits<'tcx, F, U>(move_data: &MoveData<'tcx>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: formatting
|
||
// let gcx = tcx.global_tcx(); | ||
// let erased_ty = gcx.lift(&tcx.erase_regions(&ty)).unwrap(); | ||
// if erased_ty.needs_drop(gcx, ctxt.param_env) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Dead code.
location, | ||
|path, s| Self::update_bits(sets, path, s) | ||
) | ||
|lv| place_contents_drop_state_cannot_differ(self.tcx, self.mir, lv), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern with this is that there is more-or-less only one "right thing" you can do here in this closure, right? Or is there ever a time that we want other behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If indeed there all closures are always the same, why not make on_lookup_result_bits
abstract over some type T: OnLookupResultBitHelpers
, which is only implemented by TyCtxt
or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One advantage to this approach is that its trivial to add instrumentation to the individual closures. Also, you need both the tcx
and the mir
, so to get the full effect here (in terms of avoiding having to thread either of those down) I'd have to at the very least make a struct that held both of them, rather than just passing either on its own.
Having said that, I was considering moving to a trait instead of arbitrary closures, largely because I wanted a single thing to carry both of the closures that I occasionally pass in. So I'll look into that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Oh, maybe you mean make it a method on tcx
that takes the mir
as an argument, and then pass in a closure that does |arg| self.tcx.new_method(self.mir, arg)
...? Not sure how that's better/clearer that |arg| current_func(self.tcx, self.mir, arg)
, though...)
src/librustc_mir/dataflow/mod.rs
Outdated
/// | ||
/// When its false, no local clone is constucted; instead a | ||
/// mutable reference directly into `on_entry` is passed along via | ||
/// `SetTriple.on_entry` instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the intention that, if this returns false, the on_entry
bit set will not be modified by statement_effect
and terminator_effect
(but merely used as immutable input)? If so, let's say so explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes, that is indeed my intent. Will fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(The fact that a mutable reference is passed is just an artifact of how support for the true
case is implemented.)
} | ||
} | ||
|
||
struct FindPlaceUses<'a, 'b: 'a, 'tcx: 'a> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the purpose of this struct should be documented here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think at this point this file is getting quite complex. We might consider breaking out submodules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I addressed the doc request but chose not to fragment the file into submodules; I want to wait until after the impl period to do the latter, for the sake of everyone rebasing...)
PlaceContext::Validate => false, | ||
|
||
// pure overwrites of an lvalue do not activate it. (note | ||
// LvalueContext::Call is solely about dest lvalue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: s/LvalueContext/PlaceContext/
// (This can certainly happen in valid code. I | ||
// just want to know about it in the short | ||
// term.) | ||
info!("encountered use of Place {:?} of borrow_idx {:?} \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: debug!
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops. will fix.
fn terminator_effect(&self, | ||
sets: &mut BlockSets<BorrowIndex>, | ||
location: Location) { | ||
fn statement_effect_on_activations(&self, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's mildly surprising that this function also applies the statement effect on reservations. Perhaps rename to statement_effect_on_activations_and_reservations
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Alternatively move that call into the caller, but I think it's maybe nice to have them grouped together here? I guess I could go either way, just want the name to be clear on what is affected.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I considered having that call in the caller, but I thought at some point there might be a case where the activation effect would want to observe state from both before and after the reservation effect. (This turned out not to be the case in the code so far, but I didn't want to build in an assumption that things would stay that way.)
Anyway the idea in my head is more that the ActiveBorrows bitvector holds both activations and reservations state. So maybe the problem is the naming here, I need some way to succinctly say "the combined reservation and activation state."
PhasedBorrows
maybe? And statement_effect_on_phased_borrows
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm going to change this so that there's a single method and we pass a is_activations: bool
parameter representing the context.
src/librustc_mir/borrow_check/mod.rs
Outdated
'next_borrow: for i in flow_state.borrows.elems_incoming() { | ||
// FIXME for now, just skip the activation state. | ||
if i.is_activation() { continue 'next_borrow; } | ||
let mut elems_incoming = flow_state.borrows.elems_incoming(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pre-existing, but I feel like the enclosing function here deserves a comment explaining what it does, and I would expect this (quite clever, actually) bit here to be in that comment (i.e., invokes with either a RESERVATION or ACTIVATION for each borrowed path, whichever is more specific).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(addressed by new commit injected earlier in commit history)
Ack, you are right! Even after you've pointed out several times that the activation point needs this check, I thought what I had for the reservation point would suffice. Will fix! |
Update: this test seems to compile for me:
Example:
|
☔ The latest upstream changes (presumably #46268) made this pull request unmergeable. Please resolve the merge conflicts. |
@nikomatsakis okay, pushed commit that adds support for checking the activation points explicitly against other borrows that overlap it, and added your test (with some extra variants added to show ... a current oddity in the implementation...) Going to look into tweaking the "use closures for everything" change to dataflow next, before addressing other nits (and rebasing to resolve the upstream conflicts). |
@nikomatsakis what do you think of the name |
(and is solely implemented by |
this also doubles as a rereview/question answering ping for you!
|
Made `do_dataflow` and related API `pub(crate)`.
Having the HIR local id is useful for cases like understanding the ReScope identifiers, which are now derived from the HIR local id, and thus one can map an ReScope back to the HIR node, once one knows what those local ids are.
0302cfd
to
9451942
Compare
Okay I think I have incorporated all review feedback (and gone through yet another semi-nasty rebase....) Lets get this landed! 😄 |
…taflow. This is meant to ease development of multi-stage dataflow analyses where the output from one analysis is used to initialize the state for the next; in such a context, you cannot start with `bottom_value` for all the bits.
… intrablock state. (Still musing about whether it could make sense to revise the design here to make these constraints on usage explicit.)
src/librustc_mir/borrow_check/mod.rs
Outdated
@@ -504,9 +529,9 @@ impl<'cx, 'gcx, 'tcx> DataflowResultsConsumer<'cx, 'tcx> for MirBorrowckCtxt<'cx | |||
let data = domain.borrows(); | |||
flow_state.borrows.with_elems_outgoing(|borrows| { | |||
for i in borrows { | |||
let borrow = &data[i]; | |||
// FIXME: does this need to care about reserved/active distinction? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no because it doesn't care about the borrow being immutable/mutable.
// a terminator location. | ||
return None; | ||
} | ||
|
||
let local = if let StatementKind::Assign(Place::Local(local), _) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you just switch this to be a .get
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
match self.mir[location.block].statements.get(location.statement_index) {
Statement { kind: StatementKind::Assign(...), ..) => {
// ..
src/librustc_mir/borrow_check/mod.rs
Outdated
if self.reservation_error_reported.contains(&place_span.0) { | ||
debug!("skipping access_place for activation of invalid reservation \ | ||
place: {:?} borrow_index: {:?}", place_span.0, borrow_index); | ||
return AccessErrorsReported { mutability_error: false, conflict_error: false }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
conflict_error should return true because an error had already been reported (earlier).
src/librustc_mir/borrow_check/mod.rs
Outdated
(Read(_), BorrowKind::Shared) => Control::Continue, | ||
// Obviously an activation is compatible with its own reservation; | ||
// so don't check if they interfere. | ||
(Activation(_, activating), _) if index.is_reservation() && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the idea of this? If there's a reservation going around a loop, this could actually cause unsoundness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First off: Good catch. I definitely need to think more about this.
Explanation: The reason for this check here is to ensure that the activation is not treated as a conflict with its own reservation. (Any time we reach a activating use, it is by definition going to have a reservation for that borrow also in the current flow state -- because if there wasn't a reservation in the flow state, then we would not consider the use an activation.)
But your loop example is important, and may show a big flaw in how I've approached this.
I think I've made a mistake in that the current Reservations dataflow is tracking what borrows may reach a location, but for the scenario I'm trying to catch here, it might be necessary to use a must-reach semantics. Building up that flow analysis and employing it (at least for the desired filter here) might be a way to resolve the loop example you give (because with must-reach, a control-flow merge on entry to the loop will remove the reservations that are introduced within the loop itself).
- Having said that, such an analysis is a pretty big addition to the framework here. I need to think more on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Another potential answer, suggested by @nikomatsakis, might be to treat the reservation in a loop as an error.)
Reservation(_) => { | ||
debug!("recording invalid reservation of \ | ||
place: {:?}", place_span.0); | ||
this.reservation_error_reported.insert(place_span.0.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this error suppression might actually fix #45697 if it's moved out of this loop.
r=me mod. comments |
…minators. (Same net effect as code from before; just cleaner way to get there.)
…of_scope_at_location. Instead we are "just" careful to invoke it (which sets up a bunch of kill bits) before we go into the code that sets up the gen bits. That way, when the gen bits are set up, they will override any previously set kill-bits for those reservations or activations.
…ror reported at *any* point in past.
…efore activation" In reality the currently generated MIR has at least one of the activations in a copy that occurs before the merge. But still, good to have a test, in anticipation of that potentially changing...
Instead, filter out (non-)conflicts of activiations with themselves in the same manner that we filter out non-conflict between an activation and its reservation.
Okay I think I've addressed almost all of the comments above.
(the five identifies failures are discussed in the details block below)
|
@bors r+ |
📌 Commit 159037e has been approved by |
@bors r- |
That should have been an r=arielb1 or r=nikomatsakis |
@bors r+ |
📌 Commit 159037e has been approved by |
However, it's probably a wise idea to limit reservations to assignments to temporaries. I don't think MIR construction ever does anything else, but you shouldn't be able to do this: let mut a = 0;
let mut b = 0;
let mut x: &mut u32 = &mut a;
let y = &mut x;
*y = &mut b; // this borrow should be active
// look at me, no use of `*y`, but:
use(x, &b); // should be illegal |
[MIR-borrowck] Two phase borrows This adds limited support for two-phase borrows as described in http://smallcultfollowing.com/babysteps/blog/2017/03/01/nested-method-calls-via-two-phase-borrowing/ The support is off by default; you opt into it via the flag `-Z two-phase-borrows` I have written "*limited* support" above because there are simple variants of the simple `v.push(v.len())` example that one would think should work but currently do not, such as the one documented in the test compile-fail/borrowck/two-phase-reservation-sharing-interference-2.rs (To be clear, that test is not describing something that is unsound. It is just providing an explicit example of a limitation in the implementation given in this PR. I have ideas on how to fix, but I want to land the work that is in this PR first, so that I can stop repeatedly rebasing this branch.)
☀️ Test successful - status-appveyor, status-travis |
This adds limited support for two-phase borrows as described in
http://smallcultfollowing.com/babysteps/blog/2017/03/01/nested-method-calls-via-two-phase-borrowing/
The support is off by default; you opt into it via the flag
-Z two-phase-borrows
I have written "limited support" above because there are simple variants of the simple
v.push(v.len())
example that one would think should work but currently do not, such as the one documented in the test compile-fail/borrowck/two-phase-reservation-sharing-interference-2.rs(To be clear, that test is not describing something that is unsound. It is just providing an explicit example of a limitation in the implementation given in this PR. I have ideas on how to fix, but I want to land the work that is in this PR first, so that I can stop repeatedly rebasing this branch.)